package logger

import (
	"context"
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

// Config ...
type Config struct {
	Format     string `yaml:"format"`
	Level      int8   `yaml:"level"`
	FilePath   string `yaml:"file_path"`
	MaxSize    int    `yaml:"max_size"`
	MaxBackups int    `yaml:"max_backups"`
	MaxAge     int    `yaml:"max_age"`
	Compress   bool   `yaml:"compress"`
}

// DefaultConfig ...
func DefaultConfig() *Config {
	return &Config{
		Format:     "json",
		Level:      int8(zapcore.InfoLevel),
		FilePath:   "logs/api.log",
		MaxSize:    4096,
		MaxBackups: 3,
		MaxAge:     7,
		Compress:   true,
	}
}

// DefaultLogger ...
var DefaultLogger *zap.Logger

// Middleware ...
func Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		traceId := uuid.New().String()
		logger := DefaultLogger.With(zap.String("trace_id", traceId))
		c.Set("logger", logger)

		start := time.Now()
		c.Next()
		end := time.Now()
		latency := end.Sub(start)

		path := c.Request.URL.Path

		clientIP := c.ClientIP()
		method := c.Request.Method
		statusCode := c.Writer.Status()

		logger.Info("access request",
			zap.Int("status code", statusCode),
			zap.Duration("letency", latency),
			zap.String("client ip", clientIP),
			zap.String("method", method),
			zap.String("path", path),
			zap.Any("query", c.Request.URL.Query()),
			zap.Any("request", c.Request.Body),
			zap.String("error", c.Errors.ByType(gin.ErrorTypePrivate).String()),
		)
	}
}

// Instance ...
func Instance(ctx context.Context) *zap.Logger {
	v := ctx.Value("logger")
	if logger, ok := v.(*zap.Logger); !ok {
		panic("get logger in context failure")
	} else {
		return logger
	}
}

// Setup ...
func Setup(setting *Config) error {
	format := setting.Format

	encoder, err := newEncoder(format)
	if err != nil {
		return err
	}

	fileName := setting.FilePath
	hook := lumberjack.Logger{
		Filename:   fileName,
		MaxSize:    setting.MaxSize,
		MaxBackups: setting.MaxBackups,
		MaxAge:     setting.MaxAge,
		Compress:   setting.Compress,
	}

	core := zapcore.NewCore(
		encoder,
		zapcore.NewMultiWriteSyncer(zapcore.AddSync(&hook)),
		zapcore.Level(setting.Level))

	DefaultLogger = zap.New(core)
	return nil
}

func newEncoder(format string) (zapcore.Encoder, error) {
	config := newEncoderConfig()
	switch format {
	case "json":
		return zapcore.NewJSONEncoder(config), nil
	case "console":
		return zapcore.NewConsoleEncoder(config), nil
	default:
		return nil, fmt.Errorf("unknown logging format: %s", format)
	}
}

func newEncoderConfig() zapcore.EncoderConfig {
	config := zap.NewProductionEncoderConfig()
	config.EncodeTime = zapcore.ISO8601TimeEncoder
	config.EncodeDuration = func(d time.Duration, encoder zapcore.PrimitiveArrayEncoder) {
		val := float64(d) / float64(time.Millisecond)
		encoder.AppendString(fmt.Sprintf("%.3fms", val))
	}
	config.LevelKey = "lvl"
	return config
}
